# 
#     Behaviour : define a basic behaviour
# 

from PyCarrara import Vector
from random import uniform
class Behaviour(object):
  def __init__(self,boid=None,scene=None):
    self.boid = boid
    self.scene = scene
      
    def get_acceleration(self):
      return Vector(0.0,0.0,0.0)      

    def move(self):
      if self.boid is None or self.scene is None:
        return Vector(0.0,0.0,0.0)  
      return self.acceleration         
##########################################################################################
# Rule 1: COHESION - Boids try to fly towards the centre of mass of neighbouring boids.
# The 'centre of mass' is simply the average position of all the boids. I use the term centre
# of mass by analogy with the corresponding physical formula (however we ignore individual
# masses here and treat all boids having the same mass).
#

class Cohesion(Behaviour):
    def __init__(self,factor=0.02,**args):
      super(Cohesion,self).__init__(**args)
      # Set the following to determine strength of rule 
      # Usually about 0.025 is good; smaller values for loose cohesion      
      self.factor = factor
      
# PROCEDURE COHESION pseudocode
#=========================================================================================
# PROCEDURE rule1(boid bJ)
#
# 	Vector pcJ
#
# 	FOR EACH BOID boid
# 		IF boid != bJ THEN
# 			pcJ = pcJ + boid.position
# 		END IF
# 	END
#
# 	pcJ = pcJ / N-1
#
# 	RETURN (pcJ - bJ.position) / 100
#
# END PROCEDURE
#=========================================================================================
    def get_acceleration(self):
      boids = self.scene.boids
      centre =  Vector(0.0,0.0,0.0)  
      num = 0
      for b in boids:
        if not (b == self.boid):
          centre +=  b.position
          num += 1
      if num > 0:
        if self.boid.name == "CameraFocus":
          print "cohesion",self.boid.name
          print centre/num ,self.boid.position,centre/num - self.boid.position
        centre = centre / num
      return (centre - (self.boid.position + self.boid.velocity)) * self.factor
      
##########################################################################################
# Rule 2: SEPARATION - Boids try to keep a small distance away from other objects (including 
# other boids). The purpose of this rule is to make sure boids don't collide into each  
# other. Look at each boid, and if it's within a defined distance (measured in feet) 
# of another boid move it as far away again as it already is. This is done by subtracting 
# from a vector c the displacement of each boid which is near by. We initialise c to zero 
# as we want this rule to give us a vector which when added to the current position moves 
# a boid away from those near it.
#

class Separation(Behaviour):
    def __init__(self,distance=20.0,factor=1.0,**args):
      super(Separation,self).__init__(**args)
      # Set the following to determine strength of rule 
      # Usually about 0.025 is good; smaller values for loose cohesion    
      self.distance = distance  
      self.factor = factor
# PROCEDURE SEPARATION pseudocode
#=========================================================================================
# PROCEDURE rule2(boid bJ)
#
# 	Vector c = 0;
#
# 	FOR EACH BOID boid
# 		IF boid != bJ THEN
#			IF |boid.position - bJ.position| < 100 THEN
#				c = c - (boid.position - bJ.position)
# 			END IF
#		END IF
#	END
#
# 	RETURN c
#
# END PROCEDURE
#=========================================================================================
      
    def get_acceleration(self):
      boids = self.scene.boids
      c =  Vector(0.0,0.0,0.0)  
      num = 0
      for b in boids:
        if not (b == self.boid):
          diff = (b.position + b.velocity) - (self.boid.position+self.boid.velocity)
          if diff.mag() < self.distance:
             print "distance",self.boid.name,b.name,diff.mag(),diff
             print "too close",diff.unit()* self.distance,(diff.unit()* self.distance - diff)
             c = c - (diff.unit()* self.distance - diff)
      if c.mag()>0:
        print "separation", c , c * self.factor
      return c * self.factor 

##########################################################################################
# Rule 3: ALIGNMENT - Boids try to match velocity with near boids.
# This is similar to Rule 1, however instead of averaging the positions of the other boids
# we average the velocities. We calculate a 'perceived velocity', pvJ, then add a small
# portion (about an eighth) to the boid's current velocity.
#
# PROCEDURE rule3 pseudocode
# Original pseudocode provided by Conway Parker (http://www.kfish.org/boids/pseudocode.html)
#=========================================================================================
class Alignment(Behaviour):
    def __init__(self,factor=0.125,**args):
      super(Alignment,self).__init__(**args)
      # Set the following to determine strength of rule 
      # Usually about 0.025 is good; smaller values for loose cohesion      
      self.factor = factor
# PROCEDURE alignment(boid bJ)
#
#	Vector pvJ
#
#	FOR EACH BOID boid
#		IF boid != bJ THEN
#			pvJ = pvJ + boid.velocity
#		END IF
#	END
#
#	pvJ = pvJ / N-1
#
#	RETURN (pvJ - bJ.velocity) / 8
#
# END PROCEDURE
    def get_acceleration(self):
      boids = self.scene.boids
      centre =  Vector(0.0,0.0,0.0)  
      num = 0
      for b in boids:
        if not (b == self.boid):
          centre +=  b.velocity
          num += 1
      if num > 0:
        centre = centre / num
      return (centre - self.boid.velocity) * self.factor
      


   
#=========================================================================================

# 
#   Offset Follower : stay at a relative offset of another boid
#   Used primarely for  the camera
# 
class OffsetFollower(Behaviour):  
    def __init__(self,offset=Vector(0.0,0.0,0.0),target=None,factor=0.5,**args):
      super(OffsetFollower,self).__init__(**args)
      self.offset = offset
      self.target = target
      self.factor = factor

    def get_acceleration(self):
      if self.target is None:
        return Vector(0.0,0.0,0.0)
      print "targetting",(self.target.position - self.boid.position   ) * self.factor  + self.offset, self.target.position, self.boid.position
      return ((self.target.position + self.offset ) - (self.boid.position + self.boid.velocity) ) * self.factor 
# 
#     Wandering : wander from point to point
#     go to a random point at a given distance 
#     change when the point is reached (1%)
# 
class Wandering(Behaviour):  
    def __init__(self,distance=50.0,factor=0.0,**args):
      super(Wandering,self).__init__(**args)
      self.target = Vector(0.0,0.0,0.0)
      self.distance = distance
      self.factor = factor
      
    def get_new_point(self):
      self.target = self.target + Vector(uniform(-1,1),uniform(-1,1),uniform(-1,1))*self.distance   

    def get_acceleration(self):
      if (self.target - (self.boid.position + self.boid.velocity)).mag() < (self.distance/100.0):
        self.get_new_point()

      return (self.target - (self.boid.position + self.boid.velocity)) * self.factor
       
# 
#     Containment
# 
class Containment(Behaviour):  
    def __init__(self,factor=0.0,**args):
      super(Containment,self).__init__(**args)
      self.factor = factor
      

    def get_acceleration(self):
      if self.boid.scene.container is not None:
        return self.boid.scene.container.get_intersection(self.boid.position + self.boid.velocity)
      return Vector(0.0,0.0,0.0) 
